抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Gatekeeper One

1. 题目要求

  • 1.1 越过守门人并且注册为一个参赛者来完成这一关.

    这可能有帮助:
    • 想一想你在 Telephone 和 Token 关卡学到的知识.
    • 你可以在 solidity 文档中更深入的了解 gasleft() 函数 (参见 herehere).
  • 1.2 题目代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract GatekeeperOne {

address public entrant;

modifier gateOne() {
require(msg.sender != tx.origin);
_;
}

modifier gateTwo() {
require(gasleft() % 8191 == 0);
_;
}

modifier gateThree(bytes8 _gateKey) {
require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one");
require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two");
require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three");
_;
}

function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) {
entrant = tx.origin;
return true;
}
}

2.分析

tips:参考博客

  • 2.1让我们把解释分成三个不同的部分

    门 1:msg.sendertx.origin

    要打开这扇门,我们必须了解msg.sender它们tx.origin之间的区别。

    让我们看看 Solidity 文档对这些全局变量的看法:

    • msg.sender( address): 消息的发送者(当前通话)
    • tx.origin( address): 交易的发送方(完整的调用链)

    当交易由 EOA 进行并直接与智能合约交互时,这些变量将具有相同的值。但是,如果它与中间人合约交互A,然后B通过直接调用(而不是 a delegatecall)与另一个合约交互,那么这些值将不同。

    在这种情况下:

    • msg.sender将有 EOA 地址
    • tx.origin``A将有合同的地址

    因为为了gateOne不恢复,我们需要让msg.sender != tx.origin这意味着我们必须enter从智能合约而不是直接从玩家的 EOA 调用。

    这不是挑战的一部分,但我建议您阅读我在进一步阅读中列出的关于一些安全问题和最佳实践tx.orgin以及何时不应使用它的内容。

    2号门:gasleft()

    从关于全局变量的 Solidity 文档中我们知道这是一个返回交易剩余气体gasleft() returns (uint256)的函数。

    重要的是要知道每个 Solidity 指令实际上是一系列低级 EVM 操作码的高级表示。执行操作码后GAS(在EVM 代码文档站点上阅读更多内容),返回值是执行剩余的气体量,也是GAS当前消耗2 gas的操作码。

    事情在这里变得过于复杂,因为要通过检查,gateTwo您必须调用level.enter{gas: exactAmountOfGas}(gateKey)非常特定数量的气体,以便gasleft().mod(8191)返回0(剩余的气体必须是 8191 的倍数)。

    你猜不到这个数字,因为你需要翻译 EVM 操作码中的所有 Solidity 代码,计算它们各自消耗的 gas 并浪费大量时间(除非你的目标也是掌握 EVM,但对于这个主题有还有大量其他资源,例如让我们玩 EVM 谜题——边玩边学习以太坊 EVM!)。您还需要记住,gas 成本可能会有所不同,具体取决于使用哪个 Solidity 编译器版本将代码编译为字节码以及在此过程中使用了哪些编译标志。一团糟。

    我们可以做什么?好吧,我们可以用简单的方法去暴力破解它!按照cmichel 的建议,我们可以利用我们正在使用本地测试环境(或分叉的环境)这一事实。

    我们知道交易使用的 gasenter必须至少为 8191 加上执行这些操作码所花费的所有 gas。我们可以进行范围猜测并对其进行暴力破解,直到它起作用为止。这是代码示例:

    1
    2
    3
    4
    5
    6
    for (uint256 i = 0; i <= 8191; i++) {
    try victim.enter{gas: 800000 + i}(gateKey) {
    console.log("passed with gas ->", 800000 + i);
    break;
    } catch {}
    }

    你从一个基本的 gas 值开始只是为了确保交易不会因为 Out of Gas 异常而恢复,然后你试图找到哪个 gas 值可以使交易成功。

    在我们的例子中(solidity 编译器 + 优化标志)正确的 gas 值是:802929

    关卡 3:铸造如何在 Solidity 中工作

    要解决最终关口,我们首先需要了解从一种类型到另一种类型的转换以及向下转换的工作原理。Solidity 文档对其进行了很好的解释:

    当您从较小的类型转换为较大的类型时,没有问题。所有的高位都用零填充,值不变。问题是当您将较大的类型转换为较小的类型时。根据值的不同,您可能会遇到数据丢失的情况,因为那些高阶位会丢失并被截断。例如,uint16(0x0101)257十进制的,但如果你向下转换它,uint8它将是1十进制的!

  • 2.2 参考视频 写的攻击合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface IGateKeeperOne {
function entrant() external view returns (address);
function enter(bytes8) external returns (bool);
}

contract Hack {
function enter (address _target, uint256 gas) external {
IGateKeeperOne target = IGateKeeperOne(_target);
uint16 k16 = uint16(uint160(tx.origin));
uint64 k64 = uint64(1 << 63) + uint64(k16);
bytes8 key = bytes8(k64);
require(gas < 8191, "gas > 8191");
require(target.enter{gas: 8191 * 10 + gas}(key), "failed");
}
}

3.解题

  • 3.1 获取关卡实例地址:0xAd682B7a072dc407361a23D0C9Ee9f1C16dEa187

  • 3.2 部署攻击合约,调用enter() 函数,将关卡实例地址传入enter() 函数中,并设置gas = 256

  • image-20230225001804566

  • image-20230225001926173

  • 3.3 提交案例

  • image-20230225002131618

  • 3.4 成功!!!!!

评论



政策 · 统计 | 本站使用 Volantis 主题设计